using System;
using System.Collections.Generic;
using System.IO;

namespace Atomic.Libraries.Jobs
{
	[Serializable]
	public sealed class JobManager
	{
		private ReadOnlyList<Job> jobs;
		private string name;
		private JobStatus status;
		[NonSerialized] bool postProcessing;
		[NonSerialized] private DirectoryInfo outputDirectory;
		[NonSerialized] private List<string> rollback;

		private JobManager(IEnumerable<Job> jobs, string name, DirectoryInfo outputDirectory)
		{
			this.jobs = new ReadOnlyList<Job>(jobs);
			this.name = name;
			this.outputDirectory = outputDirectory;

			status = new JobStatus(this);
			postProcessing = false;
			rollback = new List<string>();

			CheckDependencies();
		}

		/// <summary>
		/// Creates a new job manager and submits all jobs for running. Job dependencies must be listed before jobs depending on them.
		/// </summary>
		public static JobManager Submit(IEnumerable<Job> jobs, string name)
		{
			return Submit(jobs, name, new DirectoryInfo(Directory.GetCurrentDirectory()));
		}

		/// <summary>
		/// Creates a new job manager and submits all jobs for running. Job dependencies must be listed before jobs depending on them.
		/// </summary>
		public static JobManager Submit(IEnumerable<Job> jobs, string name, DirectoryInfo outputDirectory)
		{
			using (FileLock fileLock = AcquireLock(outputDirectory))
			{
				// Create new manager object. Submit and link before serializing.
				JobManager manager = new JobManager(jobs, name, outputDirectory);
				manager.Submit();
				manager.Serialize();
				return manager;
			}
		}

		/// <summary>
		/// Loads an existing saved job manager.
		/// </summary>
		public static JobManager Load()
		{
			return Load(new DirectoryInfo(Directory.GetCurrentDirectory()));
		}

		/// <summary>
		/// Loads an existing saved job manager.
		/// </summary>
		public static JobManager Load(DirectoryInfo outputDirectory)
		{
			if (!outputDirectory.Exists)
			{
				throw new Exception("Job manager directory doesn't exist.");
			}

			using (FileLock fileLock = AcquireLock(outputDirectory))
			{
				JobManager manager = Deserialize(outputDirectory);
				manager.Status.Refresh();

				// Check whether all jobs have finished.
				manager.CheckPostProcessing();

				return manager;
			}
		}

		private static FileLock AcquireLock(DirectoryInfo outputDirectory)
		{
			return FileLock.Acquire(BuildManagerFile(outputDirectory).AddFileExtension(".lock"));
		}

		/// <summary>
		/// Parses arguments from the command line and performs the action specified. An delegate for creating jobs
		/// must be specified.
		/// </summary>
		public static JobManager ParseCommandLine(string[] args, Func<IEnumerable<Job>> creator)
		{
			DirectoryInfo outputDirectory;
			if (args.Length >= 2 && args[0] == "--jobmanager" && Directory.Exists(args[1]))
			{
				outputDirectory = new DirectoryInfo(args[1]);

				// Strip used arguments from array.
				List<string> args2 = new List<string>(args);
				args2.RemoveRange(0, 2);
				args = args2.ToArray();
			}
			else
			{
				outputDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
			}

			return ParseCommandLine(args, creator, outputDirectory);
		}

		/// <summary>
		/// Parses arguments from the command line and performs the action specified. An delegate for creating jobs
		/// must be specified.
		/// </summary>
		public static JobManager ParseCommandLine(string[] args, Func<IEnumerable<Job>> creator, DirectoryInfo outputDirectory)
		{
			/*if (args.Length == 4 && args[0] == "--jobmanager" && args[2] == "--start")
			{
				// Ignoring first part here. Find a better way to do this!
				args = new string[] { args[2], args[3] };
			}*/

			string name = null;
			if (args.Length == 3 && args[0] == "--submit" && args[1] == "--name" && TryParseName(args[2], out name) || args.Length == 3 && args[2] == "--submit" && args[0] == "--name" && TryParseName(args[1], out name))
			{
				args = new string[] { "--submit" };
			}

			bool purge = false;
			int index = -1;
			if (args.Length == 3 && args[0] == "--purge" && args[1] == "--resubmit" && int.TryParse(args[2], out index) || args.Length == 3 && args[2] == "--purge" && args[0] == "--resubmit" && int.TryParse(args[1], out index))
			{
				purge = true;
				args = new string[] { "--resubmit", index.ToString() };
			}

			if (args.Length == 0)
			{
				return Load(outputDirectory);
			}
			else if (args.Length == 1 && args[0] == "--submit")
			{
				// --queue ??
				return Submit(creator(), name, outputDirectory);
			}
			/*else if (args.Length == 1 && args[0] == "--resubmit")
			{
				JobManager manager = Load(outputDirectory);
				manager.Resubmit();
				return manager;
			}*/
			else if (args.Length == 2 && args[0] == "--resubmit")
			{
				JobManager manager = Load(outputDirectory);
				manager.Jobs[int.Parse(args[1])].Resubmit(purge);
				return manager;
			}
			else if (args.Length == 1 && args[0] == "--terminate")
			{
				JobManager manager = Load(outputDirectory);
				manager.Terminate();
				return manager;
			}
			else if (args.Length == 2 && args[0] == "--terminate")
			{
				JobManager manager = Load(outputDirectory);
				manager.Jobs[int.Parse(args[1])].Terminate();
				return manager;
			}
			else if (args.Length == 1 && args[0] == "--refresh")
			{
				JobManager manager = Load(outputDirectory);
				manager.Refresh();
				return manager;
			}
			else if (args.Length == 2 && args[0] == "--start")
			{
				JobManager manager = Load(outputDirectory);
				manager.Jobs[int.Parse(args[1])].Start();
				return manager;
			}
			else
			{
				throw new NotSupportedException("Command line argument not supported.");
			}
		}

		public static bool TryParseCommandLine(string[] args)
		{
			// FIXME Make more generic!

			/*// Try this only if first argument is --jobmanager.
			if (args.Length > 1 && args[0] == "--jobmanager")
			{
				// ...
			}*/

			int index;
			if (args.Length == 4 && args[0] == "--jobmanager" && args[2] == "--start" && Directory.Exists(args[1]) && int.TryParse(args[3], out index))
			{
				JobManager manager = Load(new DirectoryInfo(args[1]));
				manager.Jobs[index].Start();
				return true;
			}

			if (args.Length == 4 && args[0] == "--jobmanager" && args[2] == "--resubmit" && Directory.Exists(args[1]) && int.TryParse(args[3], out index))
			{
				JobManager manager = Load(new DirectoryInfo(args[1]));
				manager.Jobs[index].Resubmit(false);
				return true;
			}

			if (args.Length == 5 && args[0] == "--jobmanager" && args[2] == "--resubmit" && args[4] == "--purge" && Directory.Exists(args[1]) && int.TryParse(args[3], out index))
			{
				JobManager manager = Load(new DirectoryInfo(args[1]));
				manager.Jobs[index].Resubmit(true);
				return true;
			}

			if (args.Length == 4 && args[0] == "--jobmanager" && args[2] == "--terminate" && Directory.Exists(args[1]) && int.TryParse(args[3], out index))
			{
				JobManager manager = Load(new DirectoryInfo(args[1]));
				manager.Jobs[index].Terminate();
				return true;
			}

			// Command line didn't requested the start of a job.
			return false;
		}

		private void CheckDependencies()
		{
			for (int i = 0; i < jobs.Count; i++)
			{
				foreach (Job dependency in jobs[i].Dependencies)
				{
					int j = jobs.IndexOf(dependency);
					if (j == -1 || j > i)
					{
						throw new Exception("Invalid job dependency. Job dependencies must be listed before jobs depending on them.");
					}
				}
			}
		}

		private void CheckPostProcessing()
		{
			postProcessing = true;
			foreach (Job job in jobs)
			{
				if (job.State != JobState.Finished && job.State != JobState.Failed)
				{
					postProcessing = false;
				}
			}
		}

		private static bool TryParseName(string s, out string name)
		{
			// Use regular expression to check name!
			name = s;
			return true;
		}

		private void Serialize()
		{
			// Save job.status first; making sure that it's marked as done initializing.
			status.Save();

			Serializer.SerializeCompressed(ManagerFile, this);
		}

		private static JobManager Deserialize(DirectoryInfo outputDirectory)
		{
			JobManager manager = (JobManager)Serializer.DeserializeCompressed(BuildManagerFile(outputDirectory));

			// This may change and is not serialized.
			manager.outputDirectory = outputDirectory;

			return manager;
		}

		private void Submit()
		{
			try
			{
				foreach (Job job in jobs)
				{
					job.Submit(this);
				}
			}
			catch
			{
				// Delete all successfully submitted jobs again in case of errors (e.g. serialization errors).
				foreach (string id in rollback)
				{
					Job.PbsDelete(id, true);
				}
				throw;
			}
		}

		/*public void Resubmit()
		{
			foreach (Job job in jobs)
			{
				job.Resubmit();
			}
		}*/

		/// <summary>
		/// Terminates all jobs.
		/// </summary>
		public void Terminate()
		{
			foreach (Job job in jobs)
			{
				job.Terminate();
			}
		}

		public void Refresh()
		{
			foreach (Job job in jobs)
			{
				job.Refresh();
			}
		}

		private static FileInfo BuildManagerFile(DirectoryInfo outputDirectory)
		{
			return outputDirectory.AddFile("job.manager");
		}

		internal void AddRollback(string id)
		{
			rollback.Add(id);
		}

		/// <summary>
		/// List of all jobs in the original order.
		/// </summary>
		public ReadOnlyList<Job> Jobs
		{
			get
			{
				return jobs;
			}
		}

		public string Name
		{
			get
			{
				return name;
			}
		}

		/// <summary>
		/// The job manager was loaded after all jobs have finished.
		/// </summary>
		public bool PostProcessing
		{
			get
			{
				return postProcessing;
			}
		}

		/// <summary>
		/// The working directory of the job manager.
		/// </summary>
		public DirectoryInfo OutputDirectory
		{
			get
			{
				return outputDirectory;
			}
		}

		/// <summary>
		/// The serialization file of the job manager.
		/// </summary>
		public FileInfo ManagerFile
		{
			get
			{
				return BuildManagerFile(outputDirectory);
			}
		}

		internal JobStatus Status
		{
			get
			{
				return status;
			}
		}
	}
}
